Skip to content

feat(contracts): M002 legacy seller claims migration - #982

Merged
kotevcode merged 11 commits into
contracts-update-allfrom
codex/m002-legacy-seller-claims
Sep 8, 2026
Merged

feat(contracts): M002 legacy seller claims migration#982
kotevcode merged 11 commits into
contracts-update-allfrom
codex/m002-legacy-seller-claims

Conversation

@alexanderludwig

@alexanderludwig alexanderludwig commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds M002, the migration that enables claims from the deployed AntseedSellerRewardsPool after M001 activates.

The intended release is 10% of cumulative locked rewards (1000 bps) for eligible sellers, and zero for proven wash traders. There is no vesting and no manual owner-managed eligibility list.

M002 removes both existing claim blockers:

  • Deploys and installs AntseedLegacySellerClaimPolicy using the pool owner.
  • Whitelists the pool on ANTSToken using the token owner, allowing payouts while general token transfers remain disabled.

Claim rules and accounting

  • The policy uses the required, immutable wash-trading registry's isProvenWashTrader(seller) result. Flagged sellers receive zero; their rewards remain locked.

  • The release percentage is immutable after deployment; the installer defaults to 1000 bps (10%). There is no time-based vesting.

  • The policy is stateless because the pool calls it as a view. It reconstructs cumulative rewards from V2 claim flags plus V2/V1 points, scanning epochs 0 through lastEpoch, including applicable pre-migration claims through V2. Earned points alone do not create a withdrawal entitlement.

  • For ordinary pool sellers, alreadyReleased = cumulativeRewards - currentLockedBalance. The policy returns the remaining lifetime allowance, capped by the actual locked balance:

    allowance = floor(cumulativeRewards × releaseBps / 10_000)
    claimable = min(currentLockedBalance, max(allowance - alreadyReleased, 0))
    
  • The pool reduces its stored locked balance after every withdrawal. Claiming again, choosing another recipient, or waiting longer cannot unlock another 10% of the same rewards.

  • Sellers with unclaimed legacy emissions initially receive only the allowance supported by rewards already locked. After claiming additional eligible legacy emissions into the pool, they can withdraw only the additional released share.

  • DIEM has zero locked rewards and cannot claim from the pool. The policy returns zero immediately for a zero locked balance; DIEM's historical direct payouts do not create an M002 withdrawal entitlement.

  • The policy has no owner, manual wash-trader flags, or mutable registry setter. Existing external administrative powers, including the pool owner's ability to replace the whole policy, are unchanged. Mixed future direct/locked histories remain unsupported.

Implementation

  • policies/AntseedLegacySellerClaimPolicy.sol: immutable configuration, registry-based wash exclusion, and cumulative release accounting.
  • script/migrations/M002LegacySellerClaims/Install.s.sol: keyless, idempotent installation; guards M001 activation, discovers the pool through V2, derives lastEpoch = gate.effectiveEpoch() - 1, and requires a nonzero wash registry with deployed code.
  • scripts/deployments/m002.mjs: installation state inspection, deployment-ledger recording, crash recovery, and fork-rehearsal integration. Defaults the wash-registry address from M001's pinned configuration.
  • M001/ledger helpers support reusable fork rehearsals and prevent temporary rehearsal ledgers from regenerating repository chain configuration.
  • Updates README/runbook documentation, .env.example, and CHANGELOG.

Validation — September 8, 2026

For current PR head 7ca6055f0efe34de8b185c625a98706d301d6a44:

  • 33 focused Solidity policy/migration tests passed.
  • 57 deployment CLI tests passed.
  • Earlier full-suite counts are not presented as a fresh full-suite run for this revision.

Additional local mainnet-fork validation (not yet committed or pushed)

The local fork suite, fixture, seller inventory, and detailed report are not yet part of this PR. These results exercise the current policy against actual deployed Base contracts and wash-trading state at block 51,048,693, with M002 installed only in the simulation. No mainnet transactions were sent.

All five M002-specific fork checks passed:

  1. Reconciled 201 discovered sellers, including 82 existing pool depositors, against historical deposits and current balances.
  2. Tested exact initial payouts, immediate repeat claims with different recipients, and replay after one year.
  3. Tested 644 unclaimed seller/epoch pairs, including incremental locking and withdrawals, duplicate epoch IDs, and repeated V2 submissions.
  4. Tested batch legacy claims and cumulative withdrawal limits.
  5. Tested DIEM's zero pool entitlement, replay rejection, and inability to forge locked rewards.

The real registry flags 54 sellers, including 34 existing pool depositors, who receive zero. The 48 eligible existing pool depositors have a combined initial allowance of 1,001,790.830220823917019615 ANTS. Future epoch-21 amounts remain dependent on usage before finalization.

Separate DIEM diagnostic finding: four additional local checks fail due to pre-existing downstream staker accounting, not M002 pool eligibility. Already-finalized DIEM liabilities exceed its balance by 1.580730664447246959 ANTS; including epoch 21 at frozen snapshot usage produces a 1.780069019657685820 ANTS shortfall. Payouts fail identically without M002 or the M001 registry flip. This requires separate remediation and does not give DIEM access to the locked-rewards pool. The combined focused/local run is 38 passed, 4 failed, not an all-green suite.

The direct contract-level fork checks are not a completed end-to-end deployment-CLI rehearsal: this branch's older M001 guard expects zero points policies, whereas the newer deployed configuration has one. The fork performs the authorized cutover actions directly. The current full CLI rehearsal remains to be validated against the matching M001 tooling before broadcasting.

Usage

pnpm contracts:deploy -- M002 --network base-mainnet --fork-test
pnpm contracts:deploy -- M002 --network base-mainnet --dry-run
pnpm contracts:deploy -- M002 --network base-mainnet --broadcast \
  --signer deployer=account:antseed-owner \
  --signer sellerRewardsPoolOwner=account:antseed-ops

Before broadcasting, resolve the deployment-tooling compatibility noted above, verify the intended 1000 bps configuration and wash-registry address, and follow the M001 cutover ordering, including funding any required DIEM legacy reward pots before changing emissions routing.

Unfreezes the deployed AntseedSellerRewardsPool after M001 activates.
Claims are blocked twice today: the pool has no sellerClaimPolicy, and ANTS
transfers are disabled with the pool (the transfer sender) never whitelisted.

- policies/AntseedLegacySellerClaimPolicy: stateless claim policy that
  re-derives each seller's cumulative locked ANTS from EmissionsV2/V1 state
  (epochs 0..lastEpoch, mirroring claimSellerEmissions), releases releaseBps
  (default 1538 ~ 10/65) with optional linear vesting, and returns zero for
  sellers the wash-trading registry has proven (or the owner flagged).
- script/migrations/M002LegacySellerClaims/Install.s.sol: keyless, idempotent
  install. Token owner whitelists the pool on ANTSToken (skipped when
  transfers are enabled or already whitelisted); pool owner deploys the policy
  and installs it. Guards: M001 active, pool discovered via V2,
  lastEpoch = gate.effectiveEpoch() - 1 >= MIGRATION_EPOCH, wash registry
  required with code.
- scripts/deployments/m002.mjs: single install phase, signers deployer +
  sellerRewardsPoolOwner, states ready/active/not-applicable/invalid, records
  002-legacy-seller-claims and updates current.json, finalize recovery,
  fork test that rehearses M001 then applies M002 on top.
- m001.mjs: extract rehearseM001OnFork so later migrations can layer on an
  activated M001 fork; ledger.mjs gains a canonicalRoot override and only
  regenerates chain config from the repository ledger.
- Tests: policy unit tests, M002 migration tests (frozen-before proof,
  release payout, wash trader NothingToClaim, resume paths, no-op rerun),
  runner tests for M002 classification/baseline/records.
- Docs: README section, M002 runbook, .env.example, CHANGELOG.
@alexanderludwig
alexanderludwig changed the base branch from contracts-update-all to main September 5, 2026 08:48
@alexanderludwig
alexanderludwig changed the base branch from main to contracts-update-all September 5, 2026 08:49
@kotevcode

Copy link
Copy Markdown
Contributor

@claude review the new contract

@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown

Claude finished @kotevcode's task in 3m 16s —— View job


Review: AntseedLegacySellerClaimPolicy.sol (M002)

  • Gather context (read PR diff, changed files)
  • Review AntseedLegacySellerClaimPolicy.sol
  • Review Install.s.sol migration script
  • Cross-check against tests for intended behavior
  • Post review feedback

I read the new policy contract (packages/contracts/policies/AntseedLegacySellerClaimPolicy.sol), its consumer (AntseedSellerRewardsPool.sol), the source it reconstructs state from (legacy/AntseedEmissionsV2.sol), the install script, and the test suite. This is a code-review only pass — no changes implemented.

Core accounting logic checks out

claimableSellerRewards (policies/AntseedLegacySellerClaimPolicy.sol:127-138) implements:

entitled  = cumulative * releaseBps / 10_000
released  = cumulative - currentLocked
claimable = min(currentLocked, max(entitled - released, 0))

cumulativeLocked() (L99-L119) re-derives this by scanning epochs 0..lastEpoch and recomputing the exact reward formula from AntseedEmissionsV2.claimSellerEmissions (legacy/AntseedEmissionsV2.sol:296-304), merging V1 points for epoch <= migrationEpoch. I traced this against the live contract and it matches field-for-field (sellerSharePct, maxSellerSharePct, budget/max-reward clamping). The test_migrationStartSketchOverpaysWithOnlyPreMigrationRewards/WithOldAndNewRewards tests are a nice touch — they include a deliberately-flawed alternative policy (MigrationStartClaimPolicyFixture) that only scans from the migration epoch, and prove it lets a seller double-dip. That's good evidence the chosen 0..lastEpoch scan range isn't arbitrary.

Monotonicity is correctly enforced: since cumulative only grows (immutable scan bound, epochs already claimed stay claimed) while locked only shrinks on each successful pool claim, released = cumulative - locked never decreases, so a seller can't re-claim the same 10% via a different recipient or by waiting (covered by test_releaseDoesNotIncreaseWithTime, _assertNothingMoreToWithdraw).

Documented limitation worth double-checking test coverage on

The docstring (L52-L58) flags that sellerEpochClaimed is set identically whether an epoch's reward was locked into the pool or minted directly (unlock-policy path, legacy/AntseedEmissionsV2.sol:310-319). Since cumulativeLocked() can't distinguish the two, a seller who has both direct-mint epochs and pool-locked epochs will have the direct-mint amount folded into cumulative, which only ever pushes claimable down (never up) — safe, but it means such a seller could see less than "10% of what's actually sitting in the pool for them."

test_unlockedSellerNeverUnderCounts (test/AntseedLegacySellerClaimPolicy.t.sol:356-374) exercises this with two different sellers (seller2 fully direct-mint, seller1 fully pool-locked) rather than one seller with a mix of both epoch types. Since locked == 0 short-circuits to 0 at L128 for the pure-direct-mint case, that test doesn't actually exercise the clamp math for the mixed case. Given the PR body explicitly calls out "mixed future direct/locked histories remain unsupported," it'd strengthen confidence to add one same-seller test with both an unlock-eligible epoch and a locked epoch, asserting the exact (reduced) claimable amount — mostly to pin the behavior down explicitly rather than leave it implied.

Registry is a single point of failure by design

isWashTrader (L93-L95) calls the immutable washTradingRegistry unconditionally for every claim. test_registryFailureBlocksClaims confirms that if the registry reverts (e.g. paused/bricked), every seller's pool.claim() reverts — not just wash-flagged ones. Since the policy has no owner and no way to swap the registry, the only recovery path is the pool owner replacing the entire policy via AntseedSellerRewardsPool.setSellerClaimPolicy (rewards/AntseedSellerRewardsPool.sol:81-84). This looks intentional (per the PR: "no owner, manual wash-trader flags, or mutable registry setter... pool owner's ability to replace the whole policy [is] unchanged") — flagging it mainly so it's an explicit, acknowledged operational risk rather than a surprise if the wash registry ever has downtime.

Install.s.sol guards look solid

The pre-broadcast checks (script/migrations/M002LegacySellerClaims/Install.s.sol:141-163) are thorough: registry/token cross-checks, M001-activation check, pool-owner match, wash-registry non-zero-with-code, and pinning lastEpoch to effectiveEpoch() - 1. One minor readability nit: LAST_LOCKED_EPOCH (the override) is required to exactly equal the derived effectiveEpoch - 1 (L160), so it can never actually diverge from the computed default — it functions purely as an explicit confirmation/sanity-pin for the operator rather than real configurability. Not a bug, just worth a one-line comment noting that's intentional so a future reader doesn't wonder why the override can never take effect.

Gas-wise, cumulativeLocked's loop is bounded by lastEpoch, which is fixed at install time (~21 per the PR's fork validation) and can't grow after M001 activation — no DoS concern there.

Not reviewed in depth

scripts/deployments/m002.mjs (ledger/CLI plumbing) and the README/runbook docs were skimmed but not deeply audited — the Solidity policy and its direct on-chain consumer were the focus, per "review the new contract."

@kotevcode
kotevcode merged commit 415d1da into contracts-update-all Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants